﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.SqlServer.Types;
using System.Xml;
using System.Text;

namespace OTPlus.Core
{
    public class Geo
    {

        #region GeneralFunctions

        /// <summary>
        /// Warning for use in comparisons. Many types of empty exist, beyond just multipolygon empty. 
        /// </summary>
        public static SqlGeography Empty
        {
            get { return SqlGeography.STMPolyFromText(new System.Data.SqlTypes.SqlChars("MULTIPOLYGON EMPTY"), 4326); }
        }

        public Int32 DistanceBetween(Proximity.ICoordinate p1, Proximity.ICoordinate p2)
        {
            Double x = (Double)69.1 * (p2.Lat - p1.Lat);
            Double y = (Double)53.0 * (p2.Lon - p1.Lon);

            return (Int32)Math.Round(Math.Sqrt((x * x + y * y)));
        }

        public Int32 DistanceBetween(Proximity.IPosition p1, Proximity.IPosition p2)
        {
            Double x = (Double)69.1 * (p2.Lat - p1.Lat);
            Double y = (Double)53.0 * (p2.Lon - p1.Lon);

            return (Int32)Math.Round(Math.Sqrt((x * x + y * y)));
        }

        public Double DistanceInMiles(Double distanceMeters, Int32 decimalPlacesToRound)
        {
            return Math.Round((distanceMeters * 0.000621371192), decimalPlacesToRound);
        }

        public String CompassHeading(Double bearing)
        {
            if (bearing <= 10 && bearing >= 350)
            { return "N"; }

            if (bearing >= 11 && bearing <= 45)
            { return "NNE"; }

            if (bearing >= 46 && bearing <= 80)
            { return "ENE"; }


            return "x";

        }

        public String CompassHeading(SqlGeography from, SqlGeography to)
        {
            return this.CompassHeading(this.CalculateBearing(from, to));
        }

        public Double CalculateBearing(SqlGeography from, SqlGeography to)
        {
            var latitude1 = this.ToRadians(from.Lat.ToSqlDecimal().ToDouble());
            var latitude2 = this.ToRadians(to.Lat.ToSqlDecimal().ToDouble());


            var longitudeDifference = this.ToRadians((to.Long.ToSqlDecimal().ToDouble() - from.Long.ToSqlDecimal().ToDouble()));

            var y = Math.Sin(longitudeDifference) * Math.Cos(latitude2);
            var x = Math.Cos(latitude1) * Math.Sin(latitude2) -
                   Math.Sin(latitude1) * Math.Cos(latitude2) * Math.Cos(longitudeDifference);

            return (this.ToDegrees(Math.Atan2(y, x)) + 360) % 360;
        }

        public Proximity.Location ShiftLocation(Proximity.ICoordinate original)
        {
            Random r = new Random();
            Proximity.Location l = new Proximity.Location();
            l.Lat = original.Lat + (r.Next(1, 2) * 0.01);
            l.Lon = original.Lon + (r.Next(1, 2) * 0.01);

            return l;
        }

        #endregion

        #region GeoCoding
        public GeoCodeResult Geocode(String street, String city, String state)
        {
            String url = "http://local.yahooapis.com/MapsService/V1/geocode?appid=HNy7DbbV34F8ZL1pEP9DF18hxp9ffkH1wXFee2Prq2dF.z9YMAqdsfKmVL0BCSrJEf.H&street=";
            url = url + street.Trim().Replace(" ", "+") + "&city=" + city.ToString() + "&state=" + state;

            return this.geocode(url);
        }

        public GeoCodeResult Geocode(String street, String city, String state, String zip)
        {
            String url = "http://local.yahooapis.com/MapsService/V1/geocode?appid=HNy7DbbV34F8ZL1pEP9DF18hxp9ffkH1wXFee2Prq2dF.z9YMAqdsfKmVL0BCSrJEf.H&street=";
            url = url + street.Trim().Replace(" ", "+") + "&city=" + city + "&state=" + state + "&zip=" + zip;

            return this.geocode(url);

        }

        private GeoCodeResult geocode(String url)
        {
            XmlTextReader reader = new XmlTextReader(url);
            reader.WhitespaceHandling = WhitespaceHandling.Significant;
            GeoCodeResult result = new GeoCodeResult();

            while (reader.Read())
            {
                if (reader.Name.ToString() == "Latitude")
                { result.Lat = reader.ReadElementContentAsDouble(); }

                if (reader.Name.ToString() == "Longitude")
                { result.Lon = reader.ReadElementContentAsDouble(); }

                if (reader.Name.ToString() == "Address")
                { result.Address = reader.ReadElementContentAsString(); }

                if (reader.Name.ToString() == "City")
                { result.City = reader.ReadElementContentAsString(); }

                if (reader.Name.ToString() == "State")
                { result.State = reader.ReadElementContentAsString(); }

                if (reader.Name.ToString() == "Zip")
                { result.Zip = reader.ReadElementContentAsString(); }

                if (reader.Name.ToString() == "Country")
                { result.Country = reader.ReadElementContentAsString(); }
            }

            if (result.Address == null || result.Address == String.Empty)
            { result = null; }

            return result;
        }

        #endregion

        #region Locations


        public double ToRadians(double degrees)
        {

            return degrees * (Math.PI / 180.0);

        }

        public double ToDegrees(double radians)
        {

            return (radians / Math.PI) * 180.0;

        }



        #endregion

        #region ReverseGeocoding

        public ReverseGeoCodeResult Reverse(Double lat, Double lon, ReverseGeoCodeArgument arg)
        {
            //we're going to round this off a bit
            lat = Math.Round(lat, 7, MidpointRounding.ToEven); lon = Math.Round(lon, 7, MidpointRounding.ToEven);


            ReverseGeoCodeResult result = getCachedReturnResult(new Proximity.Location(lat, lon));

            if (result != null)
            { return result; }

            if (arg == ReverseGeoCodeArgument.Anything)
            {
                //get it from the database

                //make the service call
                return this.reverseViaServiceCall(lat, lon);
            }

            if (arg == ReverseGeoCodeArgument.Serivce)
            { return this.reverseViaServiceCall(lat, lon); }

            return null;

        }

        private ReverseGeoCodeResult getCachedReturnResult(Proximity.Location l)
        {

            System.Web.Caching.Cache c = System.Web.HttpContext.Current.Cache;
            Double combo = Math.Abs(l.Lat) + Math.Abs(l.Lon);
            if (c[combo.ToString()] != null)
            {
                return (ReverseGeoCodeResult)c[combo.ToString()];
            }
            else
            { return null; }
        }

        private void setCachedReturnResult(ReverseGeoCodeResult reverseResult, Proximity.Location l)
        {
            Double combo = Math.Abs(l.Lat) + Math.Abs(l.Lon);
            System.Web.Caching.Cache c = System.Web.HttpContext.Current.Cache;
            c.Insert(combo.ToString(), reverseResult);
        }

        private ReverseGeoCodeResult reverseViaServiceCall(Double lat, Double lon)
        {
            String url = "http://ws.geonames.net/findNearestAddress?username=radpin&token=619&lat=" + lat.ToString() + "&lng=" + lon.ToString();

            System.Net.WebClient client = new System.Net.WebClient();
            System.IO.Stream stream;
            XmlTextReader reader;

            try
            {
                stream = client.OpenRead(url);

                reader = new XmlTextReader(stream);
                reader.WhitespaceHandling = WhitespaceHandling.Significant;
                ReverseGeoCodeResult result = new ReverseGeoCodeResult();

                while (reader.Read())
                {
                    if (reader.Name.ToString() == "street")
                    { result.Street = reader.ReadElementContentAsString(); }

                    if (reader.Name.ToString() == "streetNumber")
                    { result.Street = reader.ReadElementContentAsString() + " " + result.Street; }

                    if (reader.Name.ToString() == "postalcode")
                    { result.Zip = reader.ReadElementContentAsString(); }

                    if (reader.Name.ToString() == "placename")
                    { result.City = reader.ReadElementContentAsString(); }

                    if (reader.Name.ToString() == "adminName1")
                    { result.State = reader.ReadElementContentAsString(); }

                    if (reader.Name.ToString() == "countryCode")
                    { result.Country = reader.ReadElementContentAsString(); }

                }

                if (result.Street == null || result.Street == String.Empty)
                { result = null; }


                client.Dispose();
                stream.Dispose();

                return result;


            }
            catch (Exception exc)
            {
                throw exc;
            }

        }

        public Int32 UTCOffset(Proximity.IOffsetable ioff)
        {
            //http://www.earthtools.org/timezone-1.1/40.71417/-74.00639
            String url = "http://www.earthtools.org/timezone-1.1/" + ioff.Lat.ToString() + "/" + ioff.Lon.ToString();

            return this.utcoffset(url);
        }

        private Int32 utcoffset(String url)
        {
            XmlTextReader reader = new XmlTextReader(url);
            reader.WhitespaceHandling = WhitespaceHandling.Significant;
            Int32 offset = 0;

            while (reader.Read())
            {
                if (reader.Name.ToString() == "offset")
                { offset = reader.ReadElementContentAsInt(); }
            }

            return offset;
        }

        #endregion

        #region Fences

        public Fence BuildFence(Double lat, Double lon, Double miles)
        {
            return this.buildFence(lat, lon, miles);
        }

        public Fence BuildFence(Proximity.ICoordinate coords, Double miles)
        {
            return this.buildFence(coords.Lat, coords.Lon, miles);
        }

        public Fence BuildFence(Proximity.Location location, Double miles)
        {
            return this.buildFence(location.Lat, location.Lon, miles);
        }

        private Fence buildFence(Double lat, Double lon, Double miles)
        {
            Geo.Fence f = new Fence();
            f.LatHigh = lat + miles / (Double)60;
            f.LatLow = lat - miles / (Double)60;
            f.LonHigh = lon + miles / (Double)60;
            f.LonLow = lon - miles / (Double)60;

            return f;
        }

        /// <summary>
        /// A true value indicates that the argument was found within the fence.
        /// </summary>
        /// <param name="arg"></param>
        /// <param name="f"></param>
        /// <returns></returns>
        public Boolean FenceCheck(Proximity.IFencable arg, Fence f)
        {
            if (
                (arg.Lat >= f.LatLow) &&
                (arg.Lat <= f.LatHigh) &&
                (arg.Lon >= f.LonLow) &&
                (arg.Lon <= f.LonHigh)
                )
            { return true; }
            else
            { return false; }

        }

        #endregion

        #region Spatial

        public static SqlGeography BuildGeography(Proximity.ICoordinate coord)
        {
            SqlGeographyBuilder builder = new SqlGeographyBuilder();

            builder.SetSrid(4326);

            builder.BeginGeography(OpenGisGeographyType.Point);
            builder.BeginFigure(coord.Lat, coord.Lon);

            builder.EndFigure();
            builder.EndGeography();

            return builder.ConstructedGeography;
        }

        public SqlGeography BuildGeography(List<Proximity.ICoordinate> list)
        {
            return this.buildGeography(list);
        }

        public SqlGeography BuildGeography(List<String> listOfCoordinatesSeparatedByCommasLatLon)
        {
            //LINESTRING(3 4,10 50,20 25)
            Double lat; Double lon;
            StringBuilder sb = new StringBuilder();
            Int32 index = 0;
            List<Proximity.ICoordinate> args = new List<Proximity.ICoordinate>();

            sb.Append("LINESTRING(");

            foreach (String str in listOfCoordinatesSeparatedByCommasLatLon)
            {   //remember wkt is lonlat not latlon
                Double.TryParse(str.Substring(0, str.IndexOf(",")), out lat);
                Double.TryParse(str.Substring(str.IndexOf(",") + 1, str.Length - str.IndexOf(",") - 1), out lon);

                if (index != listOfCoordinatesSeparatedByCommasLatLon.Count - 1)
                {
                    sb.Append(lon.ToString() + " " + lat.ToString() + ",");

                }
                else
                {
                    sb.Append(lon.ToString() + " " + lat.ToString());
                }

                args.Add(new Proximity.Location(lat, lon));
                index++;
            }

            sb.Append(")");

            //if (SQLSpatialTools.Functions.IsValidGeographyFromText(sb.ToString(), 4326))
            //{
            //    return this.buildGeography(args);
            //}
            //else
            //{
            //    return null;
            //}

            throw new NotImplementedException();

        }

        private SqlGeography buildGeography(List<Proximity.ICoordinate> list)
        {
            SqlGeographyBuilder builder = new SqlGeographyBuilder();
            Proximity.ICoordinate start = list[0];
            Proximity.ICoordinate end = list[list.Count - 1];

            builder.SetSrid(4326);
            builder.BeginGeography(OpenGisGeographyType.Polygon);
            builder.BeginFigure(start.Lat, start.Lon);

            list.Remove(start);

            foreach (Proximity.ICoordinate coordinate in list)
            {
                builder.AddLine(coordinate.Lat, coordinate.Lon);
            }

            builder.AddLine(start.Lat, start.Lon);

            builder.EndFigure();
            builder.EndGeography();

            return builder.ConstructedGeography;
        }

        public List<SqlGeography> SeparatePolygonsFromMultipolygon(SqlGeography multi)
        {
            List<SqlGeography> list = new List<SqlGeography>();
            Int32 i = 0;// = (Int32)multipolygon.STNumGeometries().ToSqlInt16();

            while (i < (Int32)multi.STNumGeometries().ToSqlInt16())
            {
                list.Add(multi.STGeometryN(i));
                i++;
            }

            return list;
        }

        public List<Proximity.ICoordinate> ExtractCoordinates(Microsoft.SqlServer.Types.SqlGeography geography)
        {
            String ihatestrings;
            String slon; String slat;
            Double lat; Double lon;
            List<Proximity.ICoordinate> list = new List<Proximity.ICoordinate>();
            List<String> strings = new List<string>();
            String wkt = geography.ToString();
            Char split = char.Parse(",");
            //POLYGON ((-120.80566406250003 44.4023918290939, -113.8623046875 30.751277776257812, -88.8134765625 46.134170046243256, -118.52050781250001 46.920255315374519, -120.80566406250003 44.4023918290939, -120.80566406250003 44.4023918290939))

            wkt = wkt.Remove(0, wkt.IndexOf("((") + 2);
            wkt = wkt.Replace("))", String.Empty);

            strings.AddRange(wkt.Split(split));

            foreach (String item in strings)
            {
                ihatestrings = item.Trim();

                slon = ihatestrings.Substring(0, ihatestrings.IndexOf(" "));
                slat = ihatestrings.Substring(ihatestrings.IndexOf(" ") + 1, ihatestrings.Length - ihatestrings.IndexOf(" ") - 2);

                Double.TryParse(slon, out lon);
                Double.TryParse(slat, out lat);

                list.Add(new Proximity.Location(lat, lon));
            }

            return list;

        }

        #endregion

        #region ArgumentClasses

        internal class LatLong: Proximity.ICoordinate
        {
            public LatLong(double lat, double lon)
            {this.Lat = lat; this.Lon = lon;}

            public double Lat
            {
                get;set;
            }

            public double Lon
            {
                get;set;
            }
        }

        public enum ReverseGeoCodeArgument
        { Anything, Serivce, Database }

        public class ReverseGeoCodeResult
        {
            public String Street;
            public String City;
            public String State;
            public String Zip;
            public String Country;
        }

        public class GeoCodeResult
        {
            public String Address;
            public String City;
            public String State;
            public String Country;
            public Double Lat;
            public Double Lon;
            public String Zip;
        }

        public class LocationResult : Proximity.Location, Proximity.ICoordinate
        {
            internal LocationResult(String city, String region, String county, Double lat, Double lon)
            {
                base.Lat = lat; base.Lon = lon; this.City = city; this.Region = region; this.Country = county;
            }

            public String City { get; set; }
            public String Region { get; set; }
            public String Country { get; set; }
        }

        /// <summary>
        /// Represents a two dimensional box. Used for creating boundries, either for inclusion or exclusion.
        /// </summary>
        public struct Fence
        {
            public Double LatHigh;
            public Double LonHigh;
            public Double LatLow;
            public Double LonLow;
        }

        #endregion
    }
}